/**
 * 
 */
package mycsveditor.editors;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * @author dongxu
 * 
 */
public class JavaAnalysisUtil {

	private static final Pattern PACKAGE_PATTERN = Pattern
			.compile("^([^.]+)(?:\\.(.+))?");

	// TODO change me according to real application
	/**
	 * Identifies the root of source code, temporary policy. To use project as
	 * source folder, set this field to blank string
	 */
	private static final String SOURCE_ROOT = "src";

	private ITextEditor fTextEditor;

	private SearchEngine fSearchEngine;

	public JavaAnalysisUtil(ITextEditor editor) {
		fSearchEngine = new SearchEngine();
		fTextEditor = editor;
	}

	public IPackageFragment getPackage(JavaCallNames jcn) {
		return getPackage(jcn.getPkg());
	}

	/**
	 * Get package element by given package name, which presents in pkg.pkg...
	 * format. Note, the SOURCE_ROOT field identifies the root of source code
	 * must be specified. getPackage2 is a substitute for the folder dependent
	 * method
	 * 
	 * @param packageName
	 * @return null if not found
	 */
	public IPackageFragment getPackage(String packageName) {

		Matcher packageMatcher = PACKAGE_PATTERN.matcher(packageName);
		if (!packageMatcher.find()) {
			return null;
		}

		IJavaProject project = getJavaProject();
		if (project == null) {
			return null;
		}

		IPackageFragmentRoot rootPackage = getRootPackage();

		IPackageFragment result = rootPackage.getPackageFragment(packageName);
		return checkPackageResult(result);
	}

	private IPackageFragmentRoot getRootPackage() {
		IJavaProject project = getJavaProject();
		IContainer rootResource = "".equals(SOURCE_ROOT) ? project.getProject()
				: project.getProject().getFolder(SOURCE_ROOT);
		IPackageFragmentRoot rootPackage = project
				.getPackageFragmentRoot(rootResource);
		return rootPackage;
	}

	private IPackageFragment checkPackageResult(IPackageFragment result) {
		return result.exists() ? result : null;
	}

	/**
	 * Get package with java search engine, this could ignore the require of
	 * package root, but maybe a little slow
	 * 
	 * @param packageName
	 * @return null if not found
	 */
	public IPackageFragment getPackage2(String packageName) {
		SearchPattern pattern = SearchPattern.createPattern(packageName,
				IJavaSearchConstants.PACKAGE,
				IJavaSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH);
		List<SearchMatch> results = search(getJavaProject(), pattern);
		if (results.size() != 1) {
			return null;
		}
		IPackageFragment result = (IPackageFragment) results.get(0);
		return checkPackageResult(result);
	}

	public IType getClazz(JavaCallNames jcn) {
		IPackageFragment pkg = getPackage(jcn.getPkg());
		if (pkg == null) {
			return null;
		}
		return getClazz(pkg, jcn.getClazz());
	}

	/**
	 * Get IType element for java class
	 * 
	 * @param parent
	 *            the package contains the class
	 * @param name
	 *            class name
	 * @return null if not found
	 */
	public IType getClazz(IPackageFragment parent, String name) {
		IType clazz = null;
		try {
			IJavaElement[] children = parent.getChildren();
			for (IJavaElement child : children) {
				if (!(child instanceof ICompilationUnit)) {
					continue;
				}
				clazz = getClazzFromCmplUnit((ICompilationUnit) child, name);
				if (clazz != null) {
					break;
				}
			}
		} catch (JavaModelException e) {
			return null;
		}
		return clazz;
	}

	/**
	 * Internal implement for retrieve a IType from a compilation unit
	 * 
	 * @param cu
	 *            compile unit
	 * @param name
	 *            class name
	 * @return null if not found
	 */
	private IType getClazzFromCmplUnit(ICompilationUnit cu, String name) {
		IType[] types;
		try {
			types = cu.getAllTypes();
			for (IType type : types) {
				if (type.getElementName().equals(name)) {
					return type;
				}
			}
		} catch (JavaModelException e) {
			return null;
		}
		return null;
	}

	public IMethod getMethod(JavaCallNames jcn) {
		IType clazz = getClazz(jcn);
		if (clazz == null) {
			return null;
		}
		return getMethod(clazz, jcn.getMethod());
	}

	/**
	 * Get IMethod element for java method
	 * 
	 * @param clazz
	 *            class contains the method
	 * @param name
	 *            method name
	 * @return null if not found
	 */
	public IMethod getMethod(IType clazz, String name) {
		IMethod[] methods;
		try {
			methods = clazz.getMethods();
			for (IMethod method : methods) {
				if (method.getElementName().equals(name)) {
					return method;
				}
			}
		} catch (JavaModelException e) {
			return null;
		}
		return null;
	}

	/**
	 * Internal method for retrieve current java project
	 * 
	 * @return null if java nature is not enabled on current project
	 */
	private IJavaProject getJavaProject() {
		try {
			IProject project = getProject();
			String javaNatureId = "org.eclipse.jdt.core.javanature";
			if (project != null && project.isNatureEnabled(javaNatureId)) {
				return JavaCore.create(project);
			} else {
				return null;
			}
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Internal method for retrieve current project
	 * 
	 * @return null if exception occurred
	 */
	private IProject getProject() {
		try {
			IEditorInput input = fTextEditor.getEditorInput();
			IFile f = ((FileEditorInput) input).getFile();
			IProject project = f.getProject();
			return project;
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Implements SearchRequestor for java search engine work, this
	 * implementation will put all search result into a list
	 * 
	 * @author dongxu
	 * 
	 */
	private static class SearchRequesterImpl extends SearchRequestor {

		private List<SearchMatch> results;

		public SearchRequesterImpl() {
			super();
			results = new ArrayList<SearchMatch>();
		}

		public SearchRequesterImpl(List<SearchMatch> list) {
			results = list;
		}

		@Override
		public void acceptSearchMatch(SearchMatch match) throws CoreException {
			results.add(match);
		}

		public List<SearchMatch> getResults() {
			return results;
		}

	}

	public static final class MethodVisitor extends ASTVisitor {

		private MethodDeclaration result = null;

		private String fCondition;

		public MethodVisitor(String condition) {
			super();
			fCondition = condition;
		}

		@Override
		public boolean visit(MethodDeclaration node) {
			if (node.getName().toString().equals(fCondition)) {
				result = node;
			}
			return super.visit(node);
		}

		public MethodDeclaration getMethodDeclaration() {
			return result;
		}
	}

	public static final class MIVisitor extends ASTVisitor {

		private IInvocationMatcher matcher;

		private List<MethodInvocation> result;

		private String parameterName;

		private JavaAnalysisUtil javaAnalysisUtil;

		public MIVisitor(IInvocationMatcher matcher,
				List<MethodInvocation> result, String parameterName,
				JavaAnalysisUtil javaAnalysisUtil) {
			super();
			this.matcher = matcher;
			this.result = result;
			this.parameterName = parameterName;
			this.javaAnalysisUtil = javaAnalysisUtil;
		}

		@Override
		public boolean visit(MethodInvocation node) {
			if (!usesSpecifiedParameter(node)) {
				return super.visit(node);
			}
			if (matcher.matches(node)) {
				result.add(node);
				return super.visit(node);
			}
			int parameterPosition = caculateParameterPosition(node);
			if (parameterPosition == -1) {
				return super.visit(node);
			}
			MethodDeclaration md = javaAnalysisUtil
					.findMethodDeclaration((IMethod) node
							.resolveMethodBinding().getJavaElement());
			if (md != null) {
				javaAnalysisUtil.resolveMethodDeclaration(md,
						parameterPosition, matcher, result);
			}
			return super.visit(node);
		}

		private boolean usesSpecifiedParameter(MethodInvocation node) {
			Pattern p = Pattern.compile(String.format(
					"^%1$s.get\\(\"[^\"]+\"\\)", parameterName));
			Matcher m = p.matcher(node.toString());
			if (m.find()) {
				return true;
			}
			for (Object argObject : node.arguments()) {
				if (!(argObject instanceof SimpleName))
					continue;
				SimpleName nameArg = (SimpleName) argObject;
				if (nameArg.getIdentifier().equals(parameterName)) {
					return true;
				}
			}
			return false;
		}

		@SuppressWarnings("unchecked")
		private int caculateParameterPosition(MethodInvocation mi) {
			List<Object> arguments = mi.arguments();
			for (int i = 0; i < arguments.size(); i++) {
				Object argument = arguments.get(i);
				if (argument.toString().contains(parameterName)) {
					return i;
				}
			}
			return -1;
		}
	}

	/**
	 * Search for IMethod elements under specific class with specific key word
	 * 
	 * @param scope
	 *            class or package
	 * @param keyWord
	 *            supports wildcards
	 * @return
	 */
	public List<IMethod> searchMethods(IJavaElement scope, String keyWord) {
		return searchMembers(scope, keyWord, IJavaSearchConstants.METHOD);
	}

	/**
	 * Search for IType elements under specific java element() with specific key
	 * word
	 * 
	 * @param scope
	 *            package, package root or java project
	 * @param keyWord
	 *            supports wildcards
	 * @return
	 */
	public List<IType> searchTypes(IJavaElement scope, String keyWord) {
		return searchMembers(scope, keyWord, IJavaSearchConstants.CLASS);
	}

	public List<IType> searchTypes2(String pkg, String keyWord) {
		String searchStr = pkg + "*.*" + keyWord + "*";
		SearchPattern pattern = SearchPattern.createPattern(searchStr,
				IJavaSearchConstants.CLASS, IJavaSearchConstants.DECLARATIONS,
				SearchPattern.R_PATTERN_MATCH);
		List<SearchMatch> searchResult = search(getRootPackage(), pattern);
		List<IType> result = new ArrayList<IType>();
		for (SearchMatch match : searchResult) {
			result.add((IType) match.getElement());
		}
		return result;
	}

	/**
	 * Internal implements the IMemeber search
	 * 
	 * @param <T>
	 * @param scope
	 * @param keyWord
	 * @param searchFor
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private <T extends IMember> List<T> searchMembers(IJavaElement scope,
			String keyWord, int searchFor) {
		SearchPattern pattern = SearchPattern.createPattern(
				transformKeyword(keyWord), searchFor,
				IJavaSearchConstants.DECLARATIONS,
				SearchPattern.R_PATTERN_MATCH);
		List<SearchMatch> searchResult = search(scope, pattern);
		List<T> result = new ArrayList<T>();
		for (SearchMatch match : searchResult) {
			result.add((T) match.getElement());
		}
		return result;
	}

	/**
	 * Transfers inputed key word to match part of declaration by adding "*" to
	 * its head and tail
	 * 
	 * @param keyWord
	 * @return
	 */
	private String transformKeyword(String keyWord) {
		return "*" + keyWord + "*";
	}

	/**
	 * Invoke java search engine to perform a search operation
	 * 
	 * @param scopeElement
	 * @param searchPattern
	 * @return a list containing all matched search result
	 */
	public List<SearchMatch> search(IJavaElement scopeElement,
			SearchPattern searchPattern) {
		IJavaSearchScope scope = SearchEngine
				.createJavaSearchScope(new IJavaElement[] { scopeElement });
		SearchRequesterImpl requestor = new SearchRequesterImpl();
		try {
			SearchParticipant[] participants = new SearchParticipant[] { SearchEngine
					.getDefaultSearchParticipant() };
			fSearchEngine.search(searchPattern, participants, scope, requestor,
					null);
		} catch (Exception e) {
		}
		return requestor.getResults();
	}

	public List<MethodInvocation> searchMethodParameterInvocation(
			IMethod method, IInvocationMatcher matcher) {
		List<MethodInvocation> result = new ArrayList<MethodInvocation>();
		MethodDeclaration md = findMethodDeclaration(method);
		if (md != null) {
			resolveMethodDeclaration(md, 0, matcher, result);
		}
		return result;
	}

	private void resolveMethodDeclaration(MethodDeclaration md,
			int parameterPosition, IInvocationMatcher matcher,
			List<MethodInvocation> result) {
		if (md == null)
			return;
		SingleVariableDeclaration para = (SingleVariableDeclaration) md
				.parameters().get(parameterPosition);
		MIVisitor miVisitor = new MIVisitor(matcher, result, para.getName()
				.toString(), this);
		md.accept(miVisitor);
	}

	public MethodDeclaration findMethodDeclaration(IMethod method) {
		if (method.getCompilationUnit() == null) {
			return null;
		}
		ASTParser parser = ASTParser.newParser(AST.JLS3);
		parser.setKind(ASTParser.K_COMPILATION_UNIT);
		parser.setSource(method.getCompilationUnit());
		parser.setResolveBindings(true);
		CompilationUnit cu = (CompilationUnit) parser.createAST(null);
		MethodVisitor mvisitor = new MethodVisitor(method.getElementName());
		cu.accept(mvisitor);
		return mvisitor.getMethodDeclaration();
	}

}
